﻿//-----------------------------------------------------------------------
// <copyright file="WebChatDataSource.cs" company="AFS">
// Copyright (c) AFS. All rights reserved.
//
//    This file is part of Chatbox.
//
//    Chatbox is free software: you can redistribute it and/or modify
//    it under the terms of the GNU Lesser General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    any later version.
//
//    Chatbox is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//    GNU Lesser General Public License for more details.
//
//    You should have received a copy of the GNU Lesser General Public License
//    along with Chatbox. If not, see http://www.gnu.org/copyleft/lesser.html
// </copyright>
//----------------------------------------------------------------------- 

namespace ChatBox.Engine.Web
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Xml.Linq;

    using ChatBox.DomainObjects.ResultData;
    using ChatBox.DomainObjects.XmlFeedObjects;
    using ChatBox.Interfaces;
    using ChatBox.Utility;

    /// <summary>
    /// Class to retrieve chat data from the website
    /// </summary>
    public sealed class WebChatDataSource : BaseWebDataSource, IPublicChatDataSource, IDisposable
    {
        #region data
        
        /// <summary>
        ///  the interfaces exposed by the server
        /// </summary>
        private IEnumerable<XmlInterface> interfaces;

        /// <summary>
        /// What are we reading? first interfaces, then messages
        /// </summary>
        private ServerDataType serverDataType;

        /// <summary>
        /// true if the first message data has not yet been received
        /// </summary>
        private bool firstMessageData = true;

        /// <summary>
        /// The last topic string sent
        /// </summary>
        private string lastTopic;

        /// <summary>
        /// The last message id received
        /// </summary>
        private int lastMessageId;

        /// <summary>
        /// The content downloader
        /// </summary>
        private WebClientWithTimeout webClient;
        
        #endregion

        /// <summary>
        /// Initializes a new instance of the WebChatDataSource class 
        /// </summary>
        /// <param name="settingsRepository">Settings data</param>
        /// <param name="logger">interface to error logging</param>
        public WebChatDataSource(IServerSettingsRepository settingsRepository, ILogger logger)
        {
            this.ServerSettings = settingsRepository.ReadSettings();
            this.Logger = logger;
        }

        #region IChatDataSource Members

        /// <summary>
        /// tell listeners that new messages have arrived
        /// </summary>
        public event EventHandler<EventArgs<ChatDataResult>> DataReceived;

        /// <summary>
        /// Start going
        /// </summary>
        public void Start()
        {
            if (!this.IsStarted)
            {
                this.IsStarted = true;

                // read interfaces first
                this.serverDataType = ServerDataType.Interfaces;

                this.webClient = new WebClientWithTimeout();

                // make the thread
                this.ReadThread = new Thread(this.ThreadLoop);
                this.ReadThread.IsBackground = true;
                this.ReadThread.Name = "Chatbox public message read thread";

                // The thread must poll for messages at intervals, 
                // but to start it must not sleep at all until interfaces and initial messages are here
                this.SleepBetweenReads = false;
                this.ReadInterval = this.ServerSettings.PublicChatRefreshInterval;

                // Start the public message reading thread
                this.ReadThread.Start();
            }
        }

        /// <summary>
        /// IDisposable member
        /// </summary>
        public void Dispose()
        {
            this.DisposeInternal();
            GC.SuppressFinalize(this);
        }

        #endregion

        #region protected

        /// <summary>
        /// The thread has called back with an error
        /// </summary>
        /// <param name="ex">the exception</param>
        protected override void ReadThreadOnError(Exception ex)
        {
            if (this.Logger != null)
            {
                this.Logger.LogException("Web chat data read error", ex);
            }

            ChatDataResult failResult = new ChatDataResult(false, ex);
            this.SendMessageData(failResult);
        }

        #endregion

        #region private

        /// <summary>
        /// Show the received message count
        /// </summary>
        /// <param name="messageCount">the number of messages received</param>
        /// <param name="topicSet">was the topic set</param>
        /// <returns>the message</returns>
        private static string StatusMessage(int messageCount, bool topicSet)
        {
            string status = string.Empty;
            if (topicSet)
            {
                status = "Topic set";
                if (messageCount > 0)
                {
                    status += " and ";
                }
            }

            if (messageCount == 1)
            {
                status += "1 public message received";
            }
            else if (messageCount > 1)
            {
                status += messageCount + " public messages received";
            }

            return status;
        }

        /// <summary>
        /// The thread main loop
        /// </summary>
        private void ThreadLoop()
        {
            while (!this.ThreadTerminated)
            {
                string data = this.DownloadData();

                if (!string.IsNullOrEmpty(data))
                {
                    try
                    {
                        TextReader reader = new StringReader(data);
                        XDocument doc = XDocument.Load(reader);
                        this.ReadThreadDataReceived(doc);
                    }
                    catch (Exception ex)
                    {
                        // report the error, carry on 
                        this.HandleXmlParseException(ex);
                    }
                }

                // wait a bit if the thread is still going, has done the initial reads that happne all at once,
                // or if it's getting error responses wait a bit
                if ((!this.ThreadTerminated && this.SleepBetweenReads) || string.IsNullOrEmpty(data))
                {
                    Thread.Sleep(this.ReadInterval);
                }
            }
        }

        /// <summary>
        /// download data from the web server
        /// </summary>
        /// <returns>the data downloaded</returns>
        private string DownloadData()
        {
            try
            {
                this.webClient.Headers.Add("user-agent", EngineConstants.UserAgentString);

                // return this.webClient.DownloadString(this.Url);

                // Cannot do the above- we have to fiddle with the text encoding
                // because e2 lies about the xml that it sends. The headers say
                // "Content-Type: text/xml; charset=ISO-8859-1" (i.e. Latin-1)
                // but it is in fact UTF8
                byte[] data = this.webClient.DownloadData(this.GetReadUrl());
                return Encoding.UTF8.GetString(data);
            }
            catch (Exception ex)
            {
                this.HandleWebException(ex);
                return string.Empty;
            }
        }

        /// <summary>
        /// The thread had called back with data in an XDocument
        /// </summary>
        /// <param name="document">the data received</param>
        private void ReadThreadDataReceived(XDocument document)
        {
            if (this.serverDataType == ServerDataType.Interfaces)
            {
                this.ReadInterfaces(document);
            }
            else
            {
                this.ReadMessages(document);
            }
        }

        /// <summary>
        /// Read interfaces from the xml document
        /// </summary>
        /// <param name="xmlDoc">The xml document</param>
        private void ReadInterfaces(XDocument xmlDoc)
        {
            try
            {
                this.interfaces = xmlDoc.ToInterfaces();

                string messsage = string.Format(CultureInfo.InvariantCulture, "{0} interfaces read", this.interfaces.Count());
                ChatDataResult successResult = new ChatDataResult(true, messsage);
                successResult.ServerDataType = ServerDataType.Interfaces;
                successResult.Interfaces = this.interfaces;
                this.SendMessageData(successResult);

                // Got the interfaces, now read messages at intervals
                this.serverDataType = ServerDataType.PublicMessages;
            }
            catch (Exception ex)
            {
                if (this.Logger != null)
                {
                    this.Logger.LogException("Web chat data ReadInterfaces", ex);
                }
                
                // send a failure result
                ChatDataResult failResult = new ChatDataResult(false, ex);
                failResult.ServerDataType = ServerDataType.Interfaces;
                this.SendMessageData(failResult);
            }
        }

        /// <summary>
        /// Read message data from the xml
        /// </summary>
        /// <param name="xmlDoc">The xml document</param>
        private void ReadMessages(XDocument xmlDoc)
        {
            ChatDataResult messagesData = xmlDoc.ToMessages(true);

            // once we have first messages, pause between polling the server
            this.SleepBetweenReads = true;
            
            int messsageCount = messagesData.Messages.Count();
            messagesData.TopicChanged = !string.Equals(messagesData.Topic, this.lastTopic, StringComparison.OrdinalIgnoreCase);
            messagesData.ServerDataType = ServerDataType.PublicMessages;
            messagesData.Message = StatusMessage(messsageCount, messagesData.TopicChanged);

            int maxMessageReceived = messagesData.Messages.MaxMessageId();

            if (maxMessageReceived > this.lastMessageId)
            {
                this.lastMessageId = maxMessageReceived;
            }

            // notify listeners if anything has changed
            // - this is the first packet of data
            // - there are new messages
            // - topic has changed from the last time
            if (this.firstMessageData || messagesData.Messages.HasItems() || messagesData.TopicChanged)
            {
                this.firstMessageData = false;
                this.lastTopic = messagesData.Topic;
                this.SendMessageData(messagesData);
            }
        }

        /// <summary>
        /// Construct a URL to read for the current state
        /// </summary>
        /// <returns>the complete URl</returns>
        private Uri GetReadUrl()
        {
            if (this.serverDataType == ServerDataType.Interfaces)
            {
                return new Uri(this.ServerSettings.XmlTickersFullPath());
            }
            else
            {
                string result = this.ServerSettings.Server + "/node/ticker/Universal+Message+XML+Ticker?do_the_right_thing=1&nosort=1&backtime=10";
                if (this.lastMessageId > 0)
                {
                    result += "&msglimit=" + this.lastMessageId;
                }

                return new Uri(result);
            }
        }

        /// <summary>
        /// Notify listeners of message data
        /// </summary>
        /// <param name="messageData">Message data to send</param>
        private void SendMessageData(ChatDataResult messageData)
        {
            this.SetSequence(messageData);

            if (this.DataReceived != null)
            {
                this.DataReceived(this, new EventArgs<ChatDataResult>(messageData));
            }
        }

        /// <summary>
        /// Shut down the message read thread and dispose of resources
        /// </summary>
        private void DisposeInternal()
        {
            if (this.ReadThread != null)
            {
                this.ThreadTerminated = true;
                this.ReadThread = null;
            }

            this.webClient.TestDispose();
            this.webClient = null;
        }

        #endregion
    }
}
